/**
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package in.shadowfax.proswipebutton;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.Path;
import ohos.agp.utils.Color;
import ohos.agp.utils.Point;
import ohos.agp.window.service.DisplayManager;
import ohos.app.Context;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * A component used to display vector graphics.
 * Currently, only the M, L, and C instructions of the path are supported,
 * and the remaining instructions are temporarily not supported.
 */
public class VectorComponent extends Component implements Component.DrawTask, Component.EstimateSizeListener {

    private static final String ATTR_VIEWPORT_WIDTH = "viewport_width";
    private static final String ATTR_VIEWPORT_HEIGHT = "viewport_height";
    private static final String ATTR_FILL_COLOR = "fill_color";
    private static final String ATTR_PATH_DATA = "path_data";

    private final Path path = new Path();
    private final Paint paint = new Paint();

    /**
     * Graphic zoom ratio (relative px value)
     */
    private double scaleDensity;
    /**
     * Vector width
     */
    private double viewportWidth;
    /**
     * Vector height
     */
    private double viewportHeight;
    /**
     * Fill color
     */
    private Color fillColor;
    /**
     * Vector path
     */
    private String pathData;

    private final Point translatePoint = new Point();

    public VectorComponent(Context context) {
        this(context, null);
    }

    public VectorComponent(Context context, AttrSet attrSet) {
        this(context, attrSet, null);
    }

    public VectorComponent(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        addDrawTask(this);
        setEstimateSizeListener(this);
        init(attrSet);
    }

    private void init(AttrSet attrSet) {
        if (attrSet == null) {
            return;
        }
        attrSet.getAttr(ATTR_VIEWPORT_WIDTH).ifPresent(attr -> viewportWidth = attr.getFloatValue());
        attrSet.getAttr(ATTR_VIEWPORT_HEIGHT).ifPresent(attr -> viewportHeight = attr.getFloatValue());
        if (attrSet.getAttr(ATTR_FILL_COLOR).isPresent()) {
            fillColor = attrSet.getAttr(ATTR_FILL_COLOR).get().getColorValue();
        } else {
            fillColor = Color.WHITE;
        }
        attrSet.getAttr(ATTR_PATH_DATA).ifPresent(attr -> pathData = attr.getStringValue());
        paint.setColor(fillColor);
    }

    /**
     * Resolve path
     *
     * @param pathData Path value
     */
    private void parsePath(String pathData) {
        List<PathRule> rules = PathRule.parsePath(pathData);
        path.reset();
        float density = (float) scaleDensity;
        for (PathRule rule : rules) {
            rule.attachToPath(path, density);
        }
    }

    @Override
    public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) {
        int width = Component.EstimateSpec.getSize(widthEstimateConfig);
        int widthMode = Component.EstimateSpec.getMode(widthEstimateConfig);
        int height = Component.EstimateSpec.getSize(heightEstimateConfig);
        int heightMode = Component.EstimateSpec.getMode(heightEstimateConfig);

        double tempWidth, tempHeight;

        if (widthMode == EstimateSpec.NOT_EXCEED || widthMode == EstimateSpec.UNCONSTRAINT) {
            tempWidth = DisplayManager.getInstance().getDefaultDisplay(this.getContext()).get().getAttributes().densityPixels * viewportWidth;
        } else {
            tempWidth = width;
        }

        if (heightMode == EstimateSpec.NOT_EXCEED || heightMode == EstimateSpec.UNCONSTRAINT) {
            tempHeight = DisplayManager.getInstance().getDefaultDisplay(this.getContext()).get().getAttributes().densityPixels * viewportHeight;
        } else {
            tempHeight = height;
        }

        scaleDensity = Math.min(tempHeight / viewportHeight, tempWidth / viewportWidth);
        if (widthMode == EstimateSpec.PRECISE && heightMode == EstimateSpec.PRECISE) {
            setEstimatedSize(
                    Component.EstimateSpec.getSizeWithMode(width, EstimateSpec.PRECISE),
                    Component.EstimateSpec.getSizeWithMode(height, EstimateSpec.PRECISE));
        } else if (widthMode == EstimateSpec.PRECISE) {
            setEstimatedSize(Component.EstimateSpec.getSizeWithMode(width, EstimateSpec.PRECISE), Component.EstimateSpec.getSizeWithMode((int) (viewportHeight * scaleDensity), EstimateSpec.PRECISE));
        } else if (heightMode == EstimateSpec.PRECISE) {
            setEstimatedSize(Component.EstimateSpec.getSizeWithMode((int) (viewportWidth * scaleDensity), EstimateSpec.PRECISE), Component.EstimateSpec.getSizeWithMode(height, EstimateSpec.PRECISE));
        } else {
            setEstimatedSize(EstimateSpec.getSizeWithMode((int) (viewportWidth * scaleDensity), EstimateSpec.PRECISE), EstimateSpec.getSizeWithMode((int) (viewportHeight * scaleDensity), EstimateSpec.PRECISE));
        }
        translatePoint.modify((float) ((getEstimatedWidth() - viewportWidth * scaleDensity) / 2.0), (float) ((getEstimatedHeight() - viewportHeight * scaleDensity) / 2.0));
        parsePath(pathData);
        return true;
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        canvas.save();
        canvas.translate(translatePoint.getPointX(), translatePoint.getPointY());
        canvas.drawPath(path, paint);
        canvas.restore();
    }

    public void setPathData(String path) {
        if (path == null || path.isEmpty()) {
            return;
        }
        this.pathData = path;
        parsePath(pathData);
        invalidate();
    }

    public void setFillColor(int color) {
        setFillColor(new Color(color));
    }

    public void setFillColor(Color color) {
        this.fillColor = color;
        paint.setColor(fillColor);
        invalidate();
    }

    private static abstract class PathRule {
        private static final String SPLIT = ",";
        private static final char M = 'M';
        private static final char L = 'L';
        private static final char C = 'C';

        protected final List<Point> points = new ArrayList<>();

        /**
         * Append vertex value to Path
         */
        abstract void attachToPath(Path path, float density);

        abstract String getKey();

        /**
         * Resolve short path instructions.
         *
         * @param shortPath Represents the data from the beginning to the end of a command letter. For example: M1.0, 2.0
         */
        protected void parseShortPath(String shortPath) {
            if (shortPath == null || shortPath.trim().length() == 0) {
                return;
            }
            if (!shortPath.startsWith(getKey())) {
                return;
            }
            points.clear();
            String result = shortPath.replace(getKey(), "");
            String[] pointStrs = result.split(SPLIT);
            float x = 0f;
            for (int i = 0; i < pointStrs.length; i++) {
                float point = Float.parseFloat(pointStrs[i]);
                if (i % 2 == 0) {
                    x = point;
                } else {
                    points.add(new Point(x, point));
                    x = 0f;
                }
            }
        }

        /**
         * Resolve path
         *
         * @param path Path value
         * @return Path rule set
         */
        public static List<PathRule> parsePath(String path) {
            if (path == null || path.trim().length() == 0) {
                return Collections.emptyList();
            }
            String[] datas = path.split(" ");
            List<PathRule> list = new ArrayList<>(datas.length);
            for (String data : datas) {
                if (data == null || data.length() == 0) {
                    continue;
                }
                char first = data.charAt(0);
                PathRule pathRule = null;
                switch (first) {
                    case M:
                        pathRule = new PathMoveTo();
                        break;
                    case L:
                        pathRule = new PathLineTo();
                        break;
                    case C:
                        pathRule = new PathCubicTo();
                        break;
                }
                if (pathRule != null) {
                    pathRule.parseShortPath(data);
                    list.add(pathRule);
                }
            }
            return list;
        }
    }

    /**
     * Path.moveTo rule
     */
    private static class PathMoveTo extends PathRule {

        @Override
        void attachToPath(Path path, float density) {
            for (Point point : points) {
                path.moveTo(point.getPointX() * density, point.getPointY() * density);
            }
        }

        @Override
        String getKey() {
            return String.valueOf(PathRule.M);
        }

    }

    /**
     * Path.lineTo rules
     */
    private static class PathLineTo extends PathRule {
        @Override
        void attachToPath(Path path, float density) {
            for (Point point : points) {
                path.lineTo(point.getPointX() * density, point.getPointY() * density);
            }
        }

        @Override
        String getKey() {
            return String.valueOf(PathRule.L);
        }
    }

    /**
     * Path.cubicTo rules
     */
    private static class PathCubicTo extends PathRule {

        @Override
        void attachToPath(Path path, float density) {
            for (int i = 2; i < points.size(); i += 3) {
                Point first = points.get(i - 2);
                Point second = points.get(i - 1);
                Point third = points.get(i);
                path.cubicTo(first.getPointX() * density, first.getPointY() * density, second.getPointX() * density, second.getPointY() * density, third.getPointX() * density, third.getPointY() * density);
            }
        }

        @Override
        String getKey() {
            return String.valueOf(PathRule.C);
        }
    }
}
